import wrap from 'lodash/wrap'
import { request, isError, AnyData, BaseData, Param } from './request'

export * from './request'

export type ArgsToParam<Args extends any[]> = (...args: Args) => Param
export type FormatResp<Res, NewRes> = (resp: Res) => NewRes

export type Wrapper<T, WrapArgs extends any[], WrapResult> = (value: T, ...args: WrapArgs) => WrapResult
export type WrapperFn<Args extends any[], Result, WrapArgs extends any[], WrapResult> = Wrapper<(...args: Args) => Result, WrapArgs, WrapResult>

export type RequestWrapper<Args extends any[], NewArgs extends any[]> = WrapperFn<Args, Param, NewArgs, Param>
export type ResponseWrapper<Data extends AnyData, Response, NewResponse> = WrapperFn<[Data], Response, [Data], NewResponse>

export interface BaseApi<Args extends any[], Data extends AnyData, Result> {
  (...args: Args): Promise<Result>
  argsToParam: ArgsToParam<Args>
  formatRespnseFn: FormatResp<Data, Result>
  wrapRequest: <NewArgs extends any[]>(requestWrapper: RequestWrapper<Args, NewArgs>) => BaseApi<NewArgs, Data, Result>
  wrapResponse: <Args extends any[], Data extends AnyData, Result, NewResult>(this: BaseApi<Args, Data, Result>, resultWrapper: ResponseWrapper<Data, Result, NewResult>) => BaseApi<Args, Data, NewResult>
  catchErr: (...catchErrCodes: number[]) => this
}

const baseApi: Pick<BaseApi<any[], AnyData, any>, 'wrapRequest' | 'catchErr' | 'wrapResponse'> = {
  wrapRequest<Args extends any[], Data extends AnyData, NewArgs extends any[], Result>(this: BaseApi<Args, Data, Result>, requestWrapper: RequestWrapper<Args, NewArgs>) {
    return createApi<NewArgs, Data, Result>((...args: NewArgs) => {
      const wrapArgsToParam = wrap(this.argsToParam, requestWrapper)
      return wrapArgsToParam(...args)
    }, this.formatRespnseFn)
  },
  wrapResponse<Args extends any[], Data extends AnyData, Result, NewResult>(this: BaseApi<Args, Data, Result>, resultWrapper: ResponseWrapper<Data, Result, NewResult>) {
    return createApi<Args, Data, NewResult>(this.argsToParam, data => {
      const wrapFormatResponseFn = wrap(this.formatRespnseFn, resultWrapper)
      return wrapFormatResponseFn(data)
    })
  },
  catchErr<Args extends any[], Data extends AnyData, Result>(this: BaseApi<Args, Data, Result>, ...catchErrCodes: number[]) {
    return this.wrapRequest((argsToParam, ...args: Args) => {
      const param = argsToParam(...args)

      return {
        ...param,
        catchErrCodes
      }
    })
  }
}

export function createApi<Data extends AnyData>(argsToParam: ArgsToParam<[]>, formatRespFn?: FormatResp<Data, Data>): BaseApi<[], Data, Data>
export function createApi<Data extends AnyData, Result>(argsToParam: ArgsToParam<[]>, formatRespFn?: FormatResp<Data, Result>): BaseApi<[], Data, Result>
export function createApi<Args extends any[], Data extends AnyData>(argsToParam: ArgsToParam<Args>, formatRespFn?: FormatResp<Data, Data>): BaseApi<Args, Data, Data>
export function createApi<Args extends any[], Data extends AnyData, Result>(argsToParam: ArgsToParam<Args>, formatRespFn?: FormatResp<Data, Result>): BaseApi<Args, Data, Result>
export function createApi<Args extends any[], Data extends AnyData, Result>(argsToParam: ArgsToParam<Args>, formatRespFn?: FormatResp<Data, Result>): BaseApi<Args, Data, Result> {
  const api = (...args: Args) => {
    const param = argsToParam(...args)
    return request<Data>(param).then(resp => {
      if (formatRespFn) {
        return formatRespFn(resp)
      }
      return resp
    })
  }
  api.argsToParam = argsToParam

  api.prototype = Object.create(baseApi)

  return api as any
}
interface ApiData<T> {
  isError(): this is BaseData<never>
  isSuccess(): this is BaseData<T>
}

export function catchError<T>(api: Promise<T>, catchErrCodes: number[]) {
  return api
    .catch(e => e)
    .then((res: BaseData<T>) => {
      const err = isError(res)
      const data = {
        isError: () => err,
        isSuccess: () => !err
      }
      if (isError(res)) {
        if (catchErrCodes.length > 0 && catchErrCodes.indexOf(res.code) === -1) {
          throw res
        }
      }
      const apiData: ApiData<T> = {
        ...data,
        ...res
      }
      return apiData
    })
}
